 /*******************************************************************************
  * Copyright (c) 2003, 2006 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
  
 package org.eclipse.ui.internal.navigator.resources.actions;


 import java.util.ArrayList ;
 import java.util.List ;

 import org.eclipse.jface.action.Action;
 import org.eclipse.jface.action.IMenuManager;
 import org.eclipse.jface.action.MenuManager;
 import org.eclipse.swt.custom.StyledText;
 import org.eclipse.swt.graphics.GC;
 import org.eclipse.swt.graphics.Point;
 import org.eclipse.swt.graphics.Rectangle;
 import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Display;
 import org.eclipse.swt.widgets.Menu;
 import org.eclipse.swt.widgets.Table;
 import org.eclipse.swt.widgets.TableItem;
 import org.eclipse.swt.widgets.Tree;
 import org.eclipse.swt.widgets.TreeItem;
 import org.eclipse.ui.PlatformUI;
 import org.eclipse.ui.commands.ICommand;
 import org.eclipse.ui.commands.ICommandManager;
 import org.eclipse.ui.commands.IKeySequenceBinding;

 /**
  * A quick menu actions provides support to assign short cuts to sub menus.
  *
  * Derived from the class of the same name in the JDT internals.
  *
  *
  * <p>
  * <strong>EXPERIMENTAL</strong>. This class or interface has been added as
  * part of a work in progress. There is a guarantee neither that this API will
  * work nor that it will remain the same. Please do not use this API without
  * consulting with the Platform/UI team.
  * </p>
  * @since 3.2
  */
 public abstract class QuickMenuAction extends Action {

     private static final int CHAR_INDENT = 3;

     /**
      * Creates a new quick menu action with the given command id.
      *
      * @param commandId
      * the command id of the short cut used to open the sub menu
      */
     public QuickMenuAction(String commandId) {
         setActionDefinitionId(commandId);
     }

     /**
      * {@inheritDoc}
      */
     public void run() {
         Display display = Display.getCurrent();
         if (display == null) {
             return;
         }
         Control focus = display.getFocusControl();
         if (focus == null || focus.isDisposed()) {
             return;
         }

         MenuManager menu = new MenuManager();
         fillMenu(menu);
         final Menu widget = menu.createContextMenu(focus.getShell());
         Point location = computeMenuLocation(focus, widget);
         if (location == null) {
             return;
         }
         widget.setLocation(location);
         widget.setVisible(true);
         while (!widget.isDisposed() && widget.isVisible()) {
             if (!display.readAndDispatch()) {
                 display.sleep();
             }
         }
         if (!widget.isDisposed()) {
             widget.dispose();
         }
     }

     /**
      * Hook to fill a menu manager with the items of the sub menu.
      *
      * @param menu
      * the sub menu to fill
      */
     protected abstract void fillMenu(IMenuManager menu);

     /**
      * Returns the short cut assigned to the sub menu or <code>null</code> if no short cut is
      * assigned.
      *
      * @return the short cut as a human readable string or <code>null</code>
      */
     public String getShortCutString() {
         final ICommandManager commandManager = PlatformUI.getWorkbench().getCommandSupport().getCommandManager();
         final ICommand command = commandManager.getCommand(getActionDefinitionId());
         if (command.isDefined()) {
             List l = command.getKeySequenceBindings();
             if (!l.isEmpty()) {
                 IKeySequenceBinding binding = (IKeySequenceBinding) l.get(0);
                 return binding.getKeySequence().format();
             }
         }
         return null;
     }

     private Point computeMenuLocation(Control focus, Menu menu) {
         Point cursorLocation = focus.getDisplay().getCursorLocation();
         Rectangle clientArea = null;
         Point result = null;
         if (focus instanceof StyledText) {
             StyledText styledText = (StyledText) focus;
             clientArea = styledText.getClientArea();
             result = computeMenuLocation(styledText);
         } else if (focus instanceof Tree) {
             Tree tree = (Tree) focus;
             clientArea = tree.getClientArea();
             result = computeMenuLocation(tree);
         } else if (focus instanceof Table) {
             Table table = (Table) focus;
             clientArea = table.getClientArea();
             result = computeMenuLocation(table);
         }
         if (result == null) {
             result = focus.toControl(cursorLocation);
         }
         if (clientArea != null && !clientArea.contains(result)) {
             result = new Point(clientArea.x + clientArea.width / 2, clientArea.y + clientArea.height / 2);
         }
         Rectangle shellArea = focus.getShell().getClientArea();
         if (!shellArea.contains(focus.getShell().toControl(focus.toDisplay(result)))) {
             result = new Point(shellArea.x + shellArea.width / 2, shellArea.y + shellArea.height / 2);
         }
         return focus.toDisplay(result);
     }

     /**
      * Hook to compute the menu location if the focus widget is a styled text widget.
      *
      * @param text
      * the styled text widget that has the focus
      *
      * @return a widget relative position of the menu to pop up or <code>null</code> if now
      * position inside the widget can be computed
      */
     protected Point computeMenuLocation(StyledText text) {
         Point result = text.getLocationAtOffset(text.getCaretOffset());
         result.y += text.getLineHeight();
         if (!text.getClientArea().contains(result)) {
             return null;
         }
         return result;
     }

     /**
      * Hook to compute the menu location if the focus widget is a tree widget.
      *
      * @param tree
      * the tree widget that has the focus
      *
      * @return a widget relative position of the menu to pop up or <code>null</code> if now
      * position inside the widget can be computed
      */
     protected Point computeMenuLocation(Tree tree) {
         TreeItem[] items = tree.getSelection();
         Rectangle clientArea = tree.getClientArea();
         switch (items.length) {
             case 0 :
                 return null;
             case 1 :
                 Rectangle bounds = items[0].getBounds();
                 Rectangle intersect = clientArea.intersection(bounds);
                 if (intersect != null && intersect.height == bounds.height) {
                     return new Point(Math.max(0, bounds.x + getAvarageCharWith(tree) * CHAR_INDENT), bounds.y + bounds.height);
                 }
                 return null;

             default :
                 Rectangle[] rectangles = new Rectangle[items.length];
                 for (int i = 0; i < rectangles.length; i++) {
                     rectangles[i] = items[i].getBounds();
                 }
                 Point cursorLocation = tree.getDisplay().getCursorLocation();
                 Point result = findBestLocation(getIncludedPositions(rectangles, clientArea), tree.toControl(cursorLocation));
                 if (result != null) {
                     result.x = result.x + getAvarageCharWith(tree) * CHAR_INDENT;
                 }
                 return result;
         }
     }

     /**
      * Hook to compute the menu location if the focus widget is a table widget.
      *
      * @param table
      * the table widget that has the focus
      *
      * @return a widget relative position of the menu to pop up or <code>null</code> if now
      * position inside the widget can be computed
      */
     protected Point computeMenuLocation(Table table) {
         TableItem[] items = table.getSelection();
         Rectangle clientArea = table.getClientArea();
         switch (items.length) {
             case 0 : {
                 return null;
             }
             case 1 : {
                 Rectangle bounds = items[0].getBounds(0);
                 Rectangle iBounds = items[0].getImageBounds(0);
                 Rectangle intersect = clientArea.intersection(bounds);
                 if (intersect != null && intersect.height == bounds.height) {
                     return new Point(Math.max(0, bounds.x + iBounds.width + getAvarageCharWith(table) * CHAR_INDENT), bounds.y + bounds.height);
                 }
                 return null;

             }
             default : {
                 Rectangle[] rectangles = new Rectangle[items.length];
                 for (int i = 0; i < rectangles.length; i++) {
                     rectangles[i] = items[i].getBounds(0);
                 }
                 Rectangle iBounds = items[0].getImageBounds(0);
                 Point cursorLocation = table.getDisplay().getCursorLocation();
                 Point result = findBestLocation(getIncludedPositions(rectangles, clientArea), table.toControl(cursorLocation));
                 if (result != null) {
                     result.x = result.x + iBounds.width + getAvarageCharWith(table) * CHAR_INDENT;
                 }
                 return result;
             }
         }
     }

     private Point[] getIncludedPositions(Rectangle[] rectangles, Rectangle widgetBounds) {
         List result = new ArrayList ();
         for (int i = 0; i < rectangles.length; i++) {
             Rectangle rectangle = rectangles[i];
             Rectangle intersect = widgetBounds.intersection(rectangle);
             if (intersect != null && intersect.height == rectangle.height) {
                 result.add(new Point(intersect.x, intersect.y + intersect.height));
             }
         }
         return (Point[]) result.toArray(new Point[result.size()]);
     }

     private Point findBestLocation(Point[] points, Point relativeCursor) {
         Point result = null;
         double bestDist = Double.MAX_VALUE;
         for (int i = 0; i < points.length; i++) {
             Point point = points[i];
             int a = 0;
             int b = 0;
             if (point.x > relativeCursor.x) {
                 a = point.x - relativeCursor.x;
             } else {
                 a = relativeCursor.x - point.x;
             }
             if (point.y > relativeCursor.y) {
                 b = point.y - relativeCursor.y;
             } else {
                 b = relativeCursor.y - point.y;
             }
             double dist = Math.sqrt(a * a + b * b);
             if (dist < bestDist) {
                 result = point;
                 bestDist = dist;
             }
         }
         return result;
     }

     private int getAvarageCharWith(Control control) {
         GC gc = null;
         try {
             gc = new GC(control);
             return gc.getFontMetrics().getAverageCharWidth();
         } finally {
             if (gc != null) {
                 gc.dispose();
             }
         }
     }
 }

